package net.pimpas.report.processor.tree;

import java.beans.BeanInfo;
import java.beans.PropertyDescriptor;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

import javax.persistence.Entity;

import net.pimpas.logging.serviceprovider.LoggingServiceProvider;
import net.pimpas.logging.serviceprovider.LoggingServiceProviderFactoryImpl;
import net.pimpas.report.processor.tree.branch.Branch;
import net.pimpas.report.processor.tree.branch.DefaultBranch;
import net.pimpas.report.processor.tree.leaf.DefaultLeaf;
import net.pimpas.report.processor.tree.node.Node;
import net.pimpas.util.reflection.ReflectionUtil;

public class DefaultTree extends DefaultBranch implements Tree {
	
	private static final long serialVersionUID = -4997562553524927113L;
	
	private boolean scanDone;
	private static final LoggingServiceProvider logger = 
		LoggingServiceProviderFactoryImpl.getLoggingServiceProvider(DefaultTree.class);

	private String name;
	private List<String> paths;
	
	public DefaultTree(String name, Class<?> rootClass) {
		super(null, rootClass);
		this.setPaths(new ArrayList<String>());
		this.setName(name);
	}

	public String getName() {
		return name;
	}

	public List<String> getPaths() {
		return paths;
	}

	public void setName(String name) {
		this.name = name;
	}

	public void setPaths(List<String> paths) {
		this.paths = paths;
	}

	public void scanPaths() {
		if(this.isScanDone())
			return;
		
		this.runNode(this);
		this.generatePaths(this);
		this.setScanDone(true);
	}
	
	private boolean checkCycleReference( Class<?> clazz, Branch parent) {
		boolean b = false;
		while(parent != null) {
			if(parent.getPropertyClass().equals(clazz)) return true;
			if(ReflectionUtil.isSubclass(parent.getClass(), Branch.class)) {
				b = this.checkCycleReference(clazz, (Branch) parent.getParent());
				if(b) return true;
			}
			return false;			
		}
		return false;
	}

	public boolean isPathCorrect(String path) {
		if(!this.isScanDone()) logger.error("[Run scan first to get all paths for tree!]");
		return this.getPaths().contains(path);
	}
	
	private void runNode(Node node) {
		if(!ReflectionUtil.isSubclass(node.getClass(), Branch.class)) return;
		
		Branch branch = (Branch) node;
		BeanInfo info = ReflectionUtil.instanceBeanInfo(branch.getPropertyClass());
		PropertyDescriptor[] properties = info.getPropertyDescriptors();
		Class<?> returnType;
		ParameterizedType genericReturnType;
		Node n = null;
		Class<?> c = null;
		
		for(PropertyDescriptor pd: properties) {
			returnType = pd.getReadMethod().getReturnType();
			genericReturnType = 
				ReflectionUtil.isSubclass(pd.getReadMethod().getGenericReturnType().getClass(), ParameterizedType.class)
					? (ParameterizedType) pd.getReadMethod().getGenericReturnType()
					: null;
			if(ReflectionUtil.checkAnnotationInClass(Entity.class, returnType)) {
				// ok, its a entity
				logger.debug("Class ["+returnType.getName()+" is entity!]");
				n = new DefaultBranch(pd.getName(), returnType);
				branch.addNode(n);
				if(this.checkCycleReference(returnType, branch)) {
					logger.debug("Skipping cyble reference detected in ["+returnType.getName()+"]");
					continue;
				}
				
				this.runNode(n);
			} else if(genericReturnType != null && genericReturnType.getActualTypeArguments() != null) {
				// check genericReturnType
				for(Type t: genericReturnType.getActualTypeArguments()) {
					try {
						c = (Class<?>)t;
						if(ReflectionUtil.checkAnnotationInClass(Entity.class, c)) {
							logger.debug("Class ["+c.getName()+" is entity!");
							n = new DefaultBranch(pd.getName(), c);
							branch.addNode(n);
							if(this.checkCycleReference(c, branch)) {
								logger.debug("Skipping cyble reference detected in ["+c.getName()+"]");
								continue;
							}
							
							this.runNode(n);
						}
					} catch(Throwable e) {
						
					}
				}
			} else {
				n = new DefaultLeaf(pd.getName(), returnType);
				branch.addNode(n);
			}
		}
	}
	
	
	private void generatePaths(Node n) {
		if(n == null) return;
		
		if(ReflectionUtil.isSubclass(n.getClass(), Branch.class)) {
			Branch branch = (Branch) n;
			for(Node n2: branch.getNodes())
				this.generatePaths(n2);
			if(ReflectionUtil.isSubclass(n.getClass(), Tree.class)) return;
		}
		
		Node l = (Node) n;
		StringBuilder sb = new StringBuilder();
		Node parent = l.getParent();
		Stack<String> way = new Stack<String>();
		way.push(l.getPropertyName());
		
		while(parent != null) {
			if(parent.getPropertyName() != null) way.push(parent.getPropertyName()+".");
			parent = parent.getParent();
		}
		
		while(!way.isEmpty()) sb.append(way.pop());
		synchronized(this.getPaths()) {
			this.getPaths().add(sb.toString());
		}
		sb.setLength(0);
	}
	

	public boolean isScanDone() {
		return scanDone;
	}

	public void setScanDone(boolean scanDone) {
		this.scanDone = scanDone;
	}
}